package io.xxx.sync.manage

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
import com.baomidou.mybatisplus.core.toolkit.IdWorker
import io.xxx.sync.dao.*
import org.quartz.*
import org.quartz.impl.StdSchedulerFactory
import org.quartz.listeners.JobListenerSupport
import org.quartz.spi.JobFactory
import org.quartz.spi.TriggerFiredBundle
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.support.BeanDefinitionBuilder
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.context.ApplicationContext
import org.springframework.context.ApplicationContextAware
import org.springframework.context.annotation.Configuration
import org.springframework.context.support.GenericApplicationContext
import org.springframework.core.annotation.Order
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import java.net.InetAddress
import java.time.LocalDateTime
import java.util.*

@Order(1)
@Configuration
class SynchronizerManager : ApplicationRunner, ApplicationContextAware {

    @Autowired
    private lateinit var propertyMapper: SyncPropertyMapper
    private lateinit var applicationContext: GenericApplicationContext

    @Synchronized
    override fun run(args: ApplicationArguments) {
        val properties = propertyMapper.selectList(null)
        for (property in properties) {
            try {
                val beanName = property.beanName()
                val beanClass = property.beanClass()
                val builder = BeanDefinitionBuilder.genericBeanDefinition(beanClass)
                        .addConstructorArgValue(property)
                if (!applicationContext.isBeanNameInUse(beanName)) {
                    applicationContext.registerBeanDefinition(beanName, builder.beanDefinition)
                }
            } catch (e: Exception) {
                if (log.isWarnEnabled) {
                    log.warn("Create synchronizer bean[${property.id}] failed.", e)
                }
            }

            if (log.isInfoEnabled) {
                log.info("Synchronizer[${property.id}] init completed.")
            }
        }
    }

    override fun setApplicationContext(applicationContext: ApplicationContext) {
        this.applicationContext = applicationContext as GenericApplicationContext
    }

    companion object {
        val log: Logger = LoggerFactory.getLogger(SynchronizerManager::class.java)
    }
}

@Component
class JobManager : InitializingBean {

    @Autowired
    private lateinit var applicationContext: ApplicationContext

    @Autowired
    private lateinit var jobPropertyMapper: JobPropertyMapper

    @Autowired
    private lateinit var jobFactory: BeanJobFactory

    private lateinit var scheduler: Scheduler

    override fun afterPropertiesSet() {
        val properties = Properties()
        properties["org.quartz.threadPool.class"] = "org.quartz.simpl.SimpleThreadPool"
        properties["org.quartz.threadPool.threadCount"] = (Runtime.getRuntime().availableProcessors() * 4).toString()
        val schedulerFactory = StdSchedulerFactory(properties)
        scheduler = schedulerFactory.scheduler
        scheduler.setJobFactory(jobFactory)
        scheduler.start()
    }

    @Scheduled(cron = "*/1 * * * * ?")
    fun loadJobs() {
        val jobProperties = jobPropertyMapper.selectList(null)
        if (jobProperties.isEmpty()) {
            return
        }

        for (jobProperty in jobProperties) {
            val cachedJobProperty = jobPropertyCache[jobProperty.beanName]
            if (cachedJobProperty == null) {
                scheduleJob(jobProperty)
            } else {
                if (jobProperty != cachedJobProperty) {
                    scheduleJob(jobProperty, true)
                }
            }
            jobPropertyCache[jobProperty.beanName] = jobProperty
        }

        val jobNames = jobProperties.map { it.beanName }.toSet()
        jobPropertyCache.forEach { (jobName, _) ->
            if (!jobNames.contains(jobName)) {
                val jobKey = JobKey(jobName)
                scheduler.deleteJob(jobKey)
                jobPropertyCache.remove(jobKey.name)
            }
        }
    }

    private fun scheduleJob(jobProperty: JobProperty, cached: Boolean = false) {
        val bean = applicationContext.getBean(jobProperty.beanName, Job::class.java)
        val jobKey = JobKey(jobProperty.beanName)
        if (jobProperty.address == InetAddress.getLocalHost().hostAddress) {
            val jobDetail = JobBuilder.newJob(bean::class.java)
                    .withIdentity(jobKey)
                    .withDescription(jobProperty.description)
                    .storeDurably()
                    .build()
            if (cached) {
                when {
                    jobProperty.enabled -> {
                        if (scheduler.checkExists(jobKey)) {
                            scheduler.deleteJob(jobKey)
                        }
                        val trigger = jobProperty.trigger
                        trigger?.let {
                            scheduler.scheduleJob(jobDetail, it)
                        }
                        if (log.isInfoEnabled) {
                            log.info("Job[{}, {}] is {}.", jobProperty.beanName, jobProperty.description,
                                    if (scheduler.checkExists(jobKey)) "reloaded" else "started")
                        }
                    }
                    !jobProperty.enabled && scheduler.checkExists(jobKey) -> {
                        scheduler.deleteJob(jobDetail.key)
                        if (log.isInfoEnabled) {
                            log.info("Job[{}, {}] is {}.", jobProperty.beanName, jobProperty.description, "stopped")
                        }
                    }
                }
            } else {
                if (jobProperty.enabled) {
                    val trigger = jobProperty.trigger
                    trigger?.let {
                        scheduler.scheduleJob(jobDetail, it)
                        if (log.isInfoEnabled) {
                            log.info("Job[{}, {}] is started.", jobProperty.beanName, jobProperty.description)
                        }
                    }
                }
            }
        } else {
            if (scheduler.checkExists(jobKey)) {
                scheduler.deleteJob(jobKey)
                if (log.isInfoEnabled) {
                    log.info("Job[{}, {}] is stopped.", jobProperty.beanName, jobProperty.description)
                }
            }
        }
    }

    companion object {
        private val log: Logger = LoggerFactory.getLogger(JobManager::class.java)
        private val jobPropertyCache = mutableMapOf<String, JobProperty>()
    }
}

@Component
class BeanJobFactory : JobFactory {

    @Autowired
    private lateinit var applicationContext: ApplicationContext

    @Autowired
    private lateinit var jobLogMapper: JobLogMapper

    override fun newJob(bundle: TriggerFiredBundle, scheduler: Scheduler): Job {
        val jobDetail = bundle.jobDetail
        val jobClass = jobDetail.jobClass
        val listenerManager = scheduler.listenerManager
        listenerManager.addJobListener(object : JobListenerSupport() {
            override fun getName(): String = jobDetail.key.name

            override fun jobWasExecuted(context: JobExecutionContext?, jobException: JobExecutionException?) {
                jobException?.let {
                    val msg = jobException.localizedMessage
                    val length = if (msg.length > 2000) 2000 else msg.length
                    val jobLog = JobLog(IdWorker.getId(), 3, msg.substring(0, length), LocalDateTime.now())
                    jobLogMapper.insert(jobLog)
                }
            }
        })
        try {
            return applicationContext.getBean(jobDetail.key.name, jobClass)
        } catch (e: Exception) {
            throw SchedulerException("Problem instantiating class '${jobDetail.jobClass.name}'", e)
        }
    }
}

@Component
@DisallowConcurrentExecution
class Cleaner : Job {

    @Autowired
    private lateinit var scheduleMapper: SyncScheduleMapper

    @Autowired
    private lateinit var documentMapper: SyncDocumentMapper

    @Autowired
    private lateinit var jobLogMapper: JobLogMapper

    override fun execute(context: JobExecutionContext) {
        val now = LocalDateTime.now()
        val scheduleWrapper = QueryWrapper<SyncSchedule>()
                .lt("created", now.minusYears(1))
        scheduleMapper.delete(scheduleWrapper)
        val documentWrapper = QueryWrapper<SyncDocument>()
                .lt("sync_created", now.minusYears(1))
        documentMapper.delete(documentWrapper)
        val jobLogWrapper = QueryWrapper<JobLog>()
                .lt("created", now.minusYears(1))
        jobLogMapper.delete(jobLogWrapper)
    }
}